//
//  BBBlobEventController.m
//  BBTouch
//
//  Created by ben smith on 5/29/08.
//  This file is part of BBTouch.
//
//  BBTouch is free software: you can redistribute it and/or modify
//  it under the terms of the GNU Lesser General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  (at your option) any later version.

//  BBTouch is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU Lesser General Public License for more details.

//  You should have received a copy of the GNU Lesser General Public License
//  along with BBTouch.  If not, see <http://www.gnu.org/licenses/>.
// 
//  Copyright 2008 Ben Britten Smith ben@benbritten.com .
//

#import "BBBlobEventController.h"
#import "BBPixelBlob.h"
#import "BBSimpleEventDelegate.h"
#import "BBBlobEvent.h"
#import "BBMeshController.h"
#import "BBDistortionMap.h"
#import "BBTUIOEventDelegate.h"
#import "BBConfigurationController.h"


// the idea here is to make the event controller a nice generic interface for any type of
// event system you want, like TUIO, notification center, NSEvent generation, whatever
// I also want to keep it as simple as possible, so I am going to design it like the
// currently existing NSResponder mouse events, specifically down, drag and up. 
// (obv with touch there are more event types and event info than just down, drag, and up
// but for now I am going to concentrate on just those.) in order to encapsulate 
// the blob detection and tracking objects from any client objects using the
// information, we will have to implement an intermediary information-carrying object
// like NSEvent. these Blob events will simply contain the blob location and 
// a unique blob ID initially. Much like handling mouse events, it will be up to the
// client applications to handle any specifics (like rotation, speed, etc..) 
// we are soley concerned with blobDown, blobDragged, and blobUp for now.

// so, in order to do that, we will make it a delegate style design, 
// the controller will process all the raw blob info, and make decisions as to 
// what is and is not a blob event, then it will call four delegate methods:
// -(void)blobDown:(BBBlobEvent*)theBlobEvent
// -(void)blobDragged:(BBBlobEvent*)theBlobEvent
// -(void)blobUp:(BBBlobEvent*)theBlobEvent
// -(void)blobMovementFinished
// TODO: make the above into a nice interface spec

// the last one is so that the delegate can properly handle multiple blob events. 
// in a single-touch system, when the mouse events occur, it is obvious when to make 
// updates to the GUI and the models. with multi-touch, you might get two blob down events,
// then a single drag event.  If you process the drag event with the other blob having
// not moved it might be a rotation gesture, however, then you might get a second drag 
// event for the previously unmoving blob which might really have been a scale gesture 
// without a delimiter to signify that it is time to process all the event s in a certain 
// cycle, the delegate has no way to properly handly the blob events.

@implementation BBBlobEventController

id sharedBlobEventController;

-(void)awakeFromNib
{
	sharedBlobEventController = self;
	currentBlobEventNumber = 0;
	newBlobEvents = [[NSMutableArray alloc] init];
	activeBlobEvents = [[NSMutableArray alloc] init];
	[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appBegin:) name:@"NSApplicationDidFinishLaunchingNotification" object:nil];

}

-(void)appBegin:(id)sender
{
	[[BBConfigurationController configurationSettings] addObserver:self forKeyPath:@"BBGenerateTUIOFlag" options:NSKeyValueObservingOptionNew context:nil];
	// set the delegate here for now 
	[self setDefaultDelegate];
	
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
	// we need to change our event delegate
	[self setDefaultDelegate];
}

-(void)setDefaultDelegate
{
	// change from the TUIO delegate to the standard and back based on the pref
	if ([BBConfigurationController boolForSettingKey:@"BBGenerateTUIOFlag"]) {
		NSString* hostName = [BBConfigurationController objectForSettingKey:@"BBGenerateTUIOHost"];
		int portNumber = [BBConfigurationController intForSettingKey:@"BBGenerateTUIOPort"];
		BBTUIOEventDelegate * tuioDelegate = [BBTUIOEventDelegate eventDelegateWithHostName:hostName portNumber:portNumber];
		[self setDelegate:tuioDelegate];
		return;
	}
	[self setDelegate:[[[BBSimpleEventDelegate alloc] init] autorelease]];
}


// this allows any concerned object to access the NIB generated instance of this object
+(BBBlobEventController*)sharedBlobEventController
{
	return sharedBlobEventController;
}

////////////////////////////////////////////////////////////////
// this happens once a cycle
// the new blobs get parsed and sorted here
// this is the main method of this object
-(void)setBlobs:(NSArray*)newBlobs
{
	// there can be a few options: 
	// 1) new blob i havent seen, this is a blob down
	// 2) old blob has moved by some amount, this is a blob drag
	// 3) a blob i know about isnt in the supplied array, this is a blob up
	// for 1) and 2) the blob event will be marked active so that it
	// persists to the next cycle
	
	[newBlobEvents removeAllObjects]; // clear the new event store
	// mark all the current events as old so if they dont get 'touched'
	// this cycle then they get blob-upped and removed
	[activeBlobEvents makeObjectsPerformSelector:@selector(markInactive)];
	
	
	NSEnumerator * blobs = [newBlobs objectEnumerator];
	BBPixelBlob* thisBlob;
	while ((thisBlob = [blobs nextObject])) {
		// check to see if it is a blob i havent seen yet:
		BBBlobEvent * match = [self matchingEvent:thisBlob];
		if (match == nil) {
			// doesnt match any existing blobs, must be new
			[self handleNewBlob:thisBlob];
		} else {
			// we have a match! handle it
			[self handleMatch:match withBlob:thisBlob];
		}
	}
	// now we have dealt with all the new and moving blobs, we need to handle any
	// blobUp type events
	[self clearInactiveEvents];
	// now add the new blobs to our active store
	[activeBlobEvents addObjectsFromArray:newBlobEvents];
	// now send the end of cycle notification to the delegate
	[self tryBlobMovementFinished];
}

-(void)handleNewBlob:(BBPixelBlob*)aBlob
{
	// need to make a new blob event, and increment the counter
	// dont forget to convert the coordinates
	NSPoint converted;
	// first convert the point and check to see if it is even in the mesh, shunt out of the method if it is not
//	if (![[[BBMeshController sharedMeshController] distortionMap] destinationPoint:&converted fromSourcePoint:[aBlob centerPoint]]) return;
	if (![[[BBMeshController sharedMeshController] distortionMap] normalizedDestinationPoint:&converted fromSourcePoint:[aBlob centerPoint]]) return;
	
	
	BBBlobEvent * newEvent = [[BBBlobEvent alloc] initWithLocation:[aBlob centerPoint] blobID:currentBlobEventNumber++];

	[newEvent setScreenLocation:converted];
	[newEvent setRadius:[aBlob radius]];
	// send the event to the delegate
	[self tryBlobDown:newEvent];
	// add it to our new active array
	[newBlobEvents addObject:newEvent];
	[newEvent release];
}


-(void)handleMatch:(BBBlobEvent*)theEvent withBlob:(BBPixelBlob*)aBlob
{
	// first we mark it as active
	[theEvent setActive:YES];
	// these blobs match.  we only really care if the blob is moving
	// TODO: this is the second time i have had to call the distance
	// method on a single blob/event pair, this info could be cached
	// somewhere (in the event obj?) if it is a performance issue
	float distance = [self distanceBetweenEvent:theEvent andBlob:aBlob];
	// first, check for jitter. the blobs have a tendency, due to the
	// camera noise, to jump around a little bit, the jitter distance is
	// the amount the blob has to move before we think it is moving on purpose
	// and not just due to image noise
	if (distance < [[BBConfigurationController objectForSettingKey:@"BBBlobJitterDistance"] floatValue]) {
		// less than the jitter distance, just noise, ignore it
		return;
	}
	// if we are here, then the blob is on the move, we need to update the event and send a dragging message
	// this is the 3rd time i have called this method to convert (first two were in the distance method)
	// this is kinda unnecessary calculations, 
	NSPoint converted;
	//if ([[[BBMeshController sharedMeshController] distortionMap] destinationPoint:&converted fromSourcePoint:[aBlob centerPoint]]) {
	if ([[[BBMeshController sharedMeshController] distortionMap] normalizedDestinationPoint:&converted fromSourcePoint:[aBlob centerPoint]]) {
		// update the event to the new location
		[theEvent updateScreenLocation:converted];
		[theEvent setBlobLocation:[aBlob centerPoint]];
		[theEvent setRadius:[aBlob radius]];
		// call the delegate
		[self tryBlobDragged:theEvent];		
	}

}


// this is how we find old blobs that should be 'up'
// just check all the blobs in the active array, any
// that arent still active at this point are considered old
-(void)clearInactiveEvents
{
	// need to pack an array with the events we want to remove
	// since we cant take them out of an array we are iterating
	// across
	NSMutableArray * eventsToRemove = [NSMutableArray array];
	
	NSEnumerator * events = [activeBlobEvents objectEnumerator];
	BBBlobEvent* event;
	while (( event = [events nextObject])) {
		if (![event active]) {
			[eventsToRemove addObject:event];
			// send the blobUp event to the delegate
			[self tryBlobUp:event];
		}
	}
	// clear them out
	[activeBlobEvents removeObjectsInArray:eventsToRemove];
}


-(BBBlobEvent*)matchingEvent:(BBPixelBlob*)aBlob
{
	// check all my current blobs to see if the new blob 'matches' any of them
	NSEnumerator * events = [activeBlobEvents objectEnumerator];
	BBBlobEvent* event;
	while (( event = [events nextObject])) {
		if ([self event:event doesMatch:aBlob]) return event;
	}
	return nil;
}



//////////////////////////////////////////////////////////////////////////////
// checks to see if the raw blob is the same blob as the 
// one stored in the event.
// this is true if the center of the blob and the event location
// are within a certain configurable distance, this distance is the
// maximum distance a blob can travel in a single frame and still
// be the same blob. you dont want it to be too big, otherwise two blobs 
// that are close together will read as the same blob, and you also 
// dont want it to be too small either, otherwise a quickly moving 
// blob will show up as a new blob every frame. This value will
// be very dependant on the actual physical size of your surface
// a good size to start with is the average radius of finger-sized blobs 
// on your system.
//////////////
// also note: blob events hold the screen coordinates, and blob have the
// raw camera coords, so you gotta do the conversion before you can check them
-(BOOL)event:(BBBlobEvent*)aBlobEvent doesMatch:(BBPixelBlob*)aBlob
{
	float distance = [self distanceBetweenEvent:aBlobEvent andBlob:aBlob];
	// if we are closer than the matching distance, then it is the same blob
	if (distance < [[BBConfigurationController objectForSettingKey:@"BBBlobMatchingDistance"] floatValue]) return YES;
	return NO;
}

-(float)distanceBetweenEvent:(BBBlobEvent*)theEvent andBlob:(BBPixelBlob*)aBlob
{
	float xDiff = [theEvent blobLocation].x - [aBlob centerPoint].x;
	float yDiff = [theEvent blobLocation].y - [aBlob centerPoint].y;
	
	return sqrt( (xDiff * xDiff) + (yDiff * yDiff) );
}

// delegate handling methods

-(void)setDelegate:(id)anObject
{
	[anObject retain];
	[delegate release];
	delegate = anObject;
	// TODO: cache the delegate 'responds to selector' results here
	// so that i dont have to check every time, would give a slight 
	// performance increase
}

// TODO: maybe make these into a single method that takes a selector and an event
// like tryDelegateSelector:(selector)theSelector withEvent:(bbblobevent)theEvent
// note: never send my blob events, as the delegate might alter them somehow
// instead, send a copy (or I could make a 'mutable' version of the BE, and
// use that in this object and then send the non-mutable version to the
// delegate, but this is easier.

-(void)tryBlobDown:(BBBlobEvent*)theBlobEvent
{
	if ([delegate respondsToSelector:@selector(blobDown:)]) {
		[delegate blobDown:[[theBlobEvent copy] autorelease]];
	}
}

-(void)tryBlobUp:(BBBlobEvent*)theBlobEvent
{
	if ([delegate respondsToSelector:@selector(blobUp:)]) {
		[delegate blobUp:[[theBlobEvent copy] autorelease]];
	}
}

-(void)tryBlobDragged:(BBBlobEvent*)theBlobEvent
{
	if ([delegate respondsToSelector:@selector(blobDragged:)]) {
		[delegate blobDragged:[[theBlobEvent copy] autorelease]];
	}
}

-(void)tryBlobMovementFinished
{
	if ([delegate respondsToSelector:@selector(blobMovementFinished)]) {
		[delegate blobMovementFinished];
	}	
}


@end
